// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
import 'package:in_app_purchase_storekit/src/in_app_purchase_apis.dart';
import 'package:in_app_purchase_storekit/src/store_kit_wrappers/enum_converters.dart';
import 'package:in_app_purchase_storekit/store_kit_wrappers.dart';

import 'fakes/fake_storekit_platform.dart';
import 'store_kit_wrappers/sk_test_stub_objects.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  final fakeStoreKitPlatform = FakeStoreKitPlatform();
  late InAppPurchaseStoreKitPlatform iapStoreKitPlatform;

  setUpAll(() {
    setInAppPurchaseHostApis(api: fakeStoreKitPlatform);
  });

  setUp(() {
    InAppPurchaseStoreKitPlatform.enableStoreKit1();
    InAppPurchaseStoreKitPlatform.registerPlatform();
    iapStoreKitPlatform =
        InAppPurchasePlatform.instance as InAppPurchaseStoreKitPlatform;
    fakeStoreKitPlatform.reset();
  });

  tearDown(() => fakeStoreKitPlatform.reset());

  group('isAvailable', () {
    test('true', () async {
      expect(await iapStoreKitPlatform.isAvailable(), isTrue);
    });
  });

  group('query product list', () {
    test('should get product list and correct invalid identifiers', () async {
      final connection = InAppPurchaseStoreKitPlatform();
      final ProductDetailsResponse response = await connection
          .queryProductDetails(<String>{'123', '456', '789', '999'});
      final List<ProductDetails> products = response.productDetails;
      expect(products.first.id, '123');
      expect(products[1].id, '456');
      expect(products[2].id, '789');
      expect(products[2].description, '');
      expect(response.notFoundIDs, <String>['999']);
      expect(response.error, isNull);
      expect(response.productDetails.first.currencySymbol, r'$');
      expect(response.productDetails[1].currencySymbol, 'EUR');
    });

    test(
      'if query products throws error, should get error object in the response',
      () async {
        fakeStoreKitPlatform.queryProductException = PlatformException(
          code: 'error_code',
          message: 'error_message',
          details: <Object, Object>{'info': 'error_info'},
        );
        final connection = InAppPurchaseStoreKitPlatform();
        final ProductDetailsResponse response = await connection
            .queryProductDetails(<String>{'123', '456', '789'});
        expect(response.productDetails, <ProductDetails>[]);
        expect(response.notFoundIDs, <String>['123', '456', '789']);
        expect(response.error, isNotNull);
        expect(response.error!.source, kIAPSource);
        expect(response.error!.code, 'error_code');
        expect(response.error!.message, 'error_message');
        expect(response.error!.details, <Object, Object>{'info': 'error_info'});
      },
    );
  });

  group('restore purchases', () {
    test('should emit restored transactions on purchase stream', () async {
      fakeStoreKitPlatform.transactionList.insert(
        0,
        fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1'),
      );
      fakeStoreKitPlatform.transactionList.insert(
        1,
        fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2'),
      );
      final completer = Completer<List<PurchaseDetails>>();
      final Stream<List<PurchaseDetails>> stream =
          iapStoreKitPlatform.purchaseStream;

      late StreamSubscription<List<PurchaseDetails>> subscription;
      subscription = stream.listen((List<PurchaseDetails> purchaseDetailsList) {
        if (purchaseDetailsList.first.status == PurchaseStatus.restored) {
          subscription.cancel();
          completer.complete(purchaseDetailsList);
        }
      });

      await iapStoreKitPlatform.restorePurchases();
      final List<PurchaseDetails> details = await completer.future;

      expect(details.length, 2);
      for (var i = 0; i < fakeStoreKitPlatform.transactionList.length; i++) {
        final SKPaymentTransactionWrapper expected =
            fakeStoreKitPlatform.transactionList[i];
        final PurchaseDetails actual = details[i];

        expect(actual.purchaseID, expected.transactionIdentifier);
        expect(actual.verificationData, isNotNull);
        expect(actual.status, PurchaseStatus.restored);
        expect(
          actual.verificationData.localVerificationData,
          fakeStoreKitPlatform.receiptData,
        );
        expect(
          actual.verificationData.serverVerificationData,
          fakeStoreKitPlatform.receiptData,
        );
        expect(actual.pendingCompletePurchase, true);
      }
    });

    test(
      'should emit empty transaction list on purchase stream when there is nothing to restore',
      () async {
        fakeStoreKitPlatform.testRestoredTransactionsNull = true;
        final completer = Completer<List<PurchaseDetails>?>();
        final Stream<List<PurchaseDetails>> stream =
            iapStoreKitPlatform.purchaseStream;

        late StreamSubscription<List<PurchaseDetails>> subscription;
        subscription = stream.listen((
          List<PurchaseDetails> purchaseDetailsList,
        ) {
          expect(purchaseDetailsList.isEmpty, true);
          subscription.cancel();
          completer.complete();
        });

        await iapStoreKitPlatform.restorePurchases();
        await completer.future;
      },
    );

    test('should not block transaction updates', () async {
      fakeStoreKitPlatform.transactionList.insert(
        0,
        fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1'),
      );
      fakeStoreKitPlatform.transactionList.insert(
        1,
        fakeStoreKitPlatform.createPurchasedTransaction('foo', 'bar'),
      );
      fakeStoreKitPlatform.transactionList.insert(
        2,
        fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2'),
      );
      final completer = Completer<List<PurchaseDetails>>();
      final Stream<List<PurchaseDetails>> stream =
          iapStoreKitPlatform.purchaseStream;

      late StreamSubscription<List<PurchaseDetails>> subscription;
      subscription = stream.listen((List<PurchaseDetails> purchaseDetailsList) {
        if (purchaseDetailsList[1].status == PurchaseStatus.purchased) {
          completer.complete(purchaseDetailsList);
          subscription.cancel();
        }
      });
      await iapStoreKitPlatform.restorePurchases();
      final List<PurchaseDetails> details = await completer.future;
      expect(details.length, 3);
      for (var i = 0; i < fakeStoreKitPlatform.transactionList.length; i++) {
        final SKPaymentTransactionWrapper expected =
            fakeStoreKitPlatform.transactionList[i];
        final PurchaseDetails actual = details[i];

        expect(actual.purchaseID, expected.transactionIdentifier);
        expect(actual.verificationData, isNotNull);
        expect(
          actual.status,
          const SKTransactionStatusConverter().toPurchaseStatus(
            expected.transactionState,
            expected.error,
          ),
        );
        expect(
          actual.verificationData.localVerificationData,
          fakeStoreKitPlatform.receiptData,
        );
        expect(
          actual.verificationData.serverVerificationData,
          fakeStoreKitPlatform.receiptData,
        );
        expect(actual.pendingCompletePurchase, true);
      }
    });

    test(
      'should emit empty transaction if transactions array does not contain a transaction with PurchaseStatus.restored status.',
      () async {
        fakeStoreKitPlatform.transactionList.insert(
          0,
          fakeStoreKitPlatform.createPurchasedTransaction('foo', 'bar'),
        );
        final completer = Completer<List<List<PurchaseDetails>>>();
        final Stream<List<PurchaseDetails>> stream =
            iapStoreKitPlatform.purchaseStream;
        final purchaseDetails = <List<PurchaseDetails>>[];

        late StreamSubscription<List<PurchaseDetails>> subscription;
        subscription = stream.listen((
          List<PurchaseDetails> purchaseDetailsList,
        ) {
          purchaseDetails.add(purchaseDetailsList);

          if (purchaseDetails.length == 2) {
            completer.complete(purchaseDetails);
            subscription.cancel();
          }
        });
        await iapStoreKitPlatform.restorePurchases();
        final List<List<PurchaseDetails>> details = await completer.future;
        expect(details.length, 2);
        expect(details[0], <List<PurchaseDetails>>[]);
        for (var i = 0; i < fakeStoreKitPlatform.transactionList.length; i++) {
          final SKPaymentTransactionWrapper expected =
              fakeStoreKitPlatform.transactionList[i];
          final PurchaseDetails actual = details[1][i];

          expect(actual.purchaseID, expected.transactionIdentifier);
          expect(actual.verificationData, isNotNull);
          expect(
            actual.status,
            const SKTransactionStatusConverter().toPurchaseStatus(
              expected.transactionState,
              expected.error,
            ),
          );
          expect(
            actual.verificationData.localVerificationData,
            fakeStoreKitPlatform.receiptData,
          );
          expect(
            actual.verificationData.serverVerificationData,
            fakeStoreKitPlatform.receiptData,
          );
          expect(actual.pendingCompletePurchase, true);
        }
      },
    );

    test(
      'receipt error should populate null to verificationData.data',
      () async {
        fakeStoreKitPlatform.transactionList.insert(
          0,
          fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT1'),
        );
        fakeStoreKitPlatform.transactionList.insert(
          1,
          fakeStoreKitPlatform.createRestoredTransaction('foo', 'RT2'),
        );
        fakeStoreKitPlatform.receiptData = null;
        final completer = Completer<List<PurchaseDetails>>();
        final Stream<List<PurchaseDetails>> stream =
            iapStoreKitPlatform.purchaseStream;

        late StreamSubscription<List<PurchaseDetails>> subscription;
        subscription = stream.listen((
          List<PurchaseDetails> purchaseDetailsList,
        ) {
          if (purchaseDetailsList.first.status == PurchaseStatus.restored) {
            completer.complete(purchaseDetailsList);
            subscription.cancel();
          }
        });

        await iapStoreKitPlatform.restorePurchases();
        final List<PurchaseDetails> details = await completer.future;

        for (final purchase in details) {
          expect(purchase.verificationData.localVerificationData, isEmpty);
          expect(purchase.verificationData.serverVerificationData, isEmpty);
        }
      },
    );

    test('test restore error', () {
      fakeStoreKitPlatform.testRestoredError = const SKError(
        code: 123,
        domain: 'error_test',
        userInfo: <String, dynamic>{'message': 'errorMessage'},
      );

      expect(
        () => iapStoreKitPlatform.restorePurchases(),
        throwsA(
          isA<SKError>()
              .having((SKError error) => error.code, 'code', 123)
              .having((SKError error) => error.domain, 'domain', 'error_test')
              .having(
                (SKError error) => error.userInfo,
                'userInfo',
                <String, dynamic>{'message': 'errorMessage'},
              ),
        ),
      );
    });
  });

  group('make payment', () {
    test(
      'buying non consumable, should get purchase objects in the purchase update callback',
      () async {
        final details = <PurchaseDetails>[];
        final completer = Completer<List<PurchaseDetails>>();
        final Stream<List<PurchaseDetails>> stream =
            iapStoreKitPlatform.purchaseStream;

        late StreamSubscription<List<PurchaseDetails>> subscription;
        subscription = stream.listen((
          List<PurchaseDetails> purchaseDetailsList,
        ) {
          details.addAll(purchaseDetailsList);
          if (purchaseDetailsList.first.status == PurchaseStatus.purchased) {
            completer.complete(details);
            subscription.cancel();
          }
        });
        final purchaseParam = AppStorePurchaseParam(
          productDetails: AppStoreProductDetails.fromSKProduct(
            dummyProductWrapper,
          ),
          applicationUserName: 'appName',
        );
        await iapStoreKitPlatform.buyNonConsumable(
          purchaseParam: purchaseParam,
        );

        final List<PurchaseDetails> result = await completer.future;
        expect(result.length, 2);
        expect(result.first.productID, dummyProductWrapper.productIdentifier);
      },
    );

    test(
      'buying consumable, should get purchase objects in the purchase update callback',
      () async {
        final details = <PurchaseDetails>[];
        final completer = Completer<List<PurchaseDetails>>();
        final Stream<List<PurchaseDetails>> stream =
            iapStoreKitPlatform.purchaseStream;

        late StreamSubscription<List<PurchaseDetails>> subscription;
        subscription = stream.listen((
          List<PurchaseDetails> purchaseDetailsList,
        ) {
          details.addAll(purchaseDetailsList);
          if (purchaseDetailsList.first.status == PurchaseStatus.purchased) {
            completer.complete(details);
            subscription.cancel();
          }
        });
        final purchaseParam = AppStorePurchaseParam(
          productDetails: AppStoreProductDetails.fromSKProduct(
            dummyProductWrapper,
          ),
          applicationUserName: 'appName',
        );
        await iapStoreKitPlatform.buyConsumable(purchaseParam: purchaseParam);

        final List<PurchaseDetails> result = await completer.future;
        expect(result.length, 2);
        expect(result.first.productID, dummyProductWrapper.productIdentifier);
      },
    );

    test('buying consumable, should throw when autoConsume is false', () async {
      final purchaseParam = AppStorePurchaseParam(
        productDetails: AppStoreProductDetails.fromSKProduct(
          dummyProductWrapper,
        ),
        applicationUserName: 'appName',
      );
      expect(
        () => iapStoreKitPlatform.buyConsumable(
          purchaseParam: purchaseParam,
          autoConsume: false,
        ),
        throwsA(isInstanceOf<AssertionError>()),
      );
    });

    test('should get failed purchase status', () async {
      fakeStoreKitPlatform.testTransactionFail = true;
      final details = <PurchaseDetails>[];
      final completer = Completer<IAPError>();
      late IAPError error;

      final Stream<List<PurchaseDetails>> stream =
          iapStoreKitPlatform.purchaseStream;
      late StreamSubscription<List<PurchaseDetails>> subscription;
      subscription = stream.listen((List<PurchaseDetails> purchaseDetailsList) {
        details.addAll(purchaseDetailsList);
        for (final purchaseDetails in purchaseDetailsList) {
          if (purchaseDetails.status == PurchaseStatus.error) {
            error = purchaseDetails.error!;
            completer.complete(error);
            subscription.cancel();
          }
        }
      });
      final purchaseParam = AppStorePurchaseParam(
        productDetails: AppStoreProductDetails.fromSKProduct(
          dummyProductWrapper,
        ),
        applicationUserName: 'appName',
      );
      await iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam);

      final IAPError completerError = await completer.future;
      expect(completerError.code, 'purchase_error');
      expect(completerError.source, kIAPSource);
      expect(completerError.message, 'ios_domain');
      expect(completerError.details, <Object, Object>{
        'message': 'an error message',
      });
    });

    test(
      'should get canceled purchase status when error code is SKErrorPaymentCancelled',
      () async {
        fakeStoreKitPlatform.testTransactionCancel = 2;
        final details = <PurchaseDetails>[];
        final completer = Completer<PurchaseStatus>();

        final Stream<List<PurchaseDetails>> stream =
            iapStoreKitPlatform.purchaseStream;
        late StreamSubscription<List<PurchaseDetails>> subscription;
        subscription = stream.listen((
          List<PurchaseDetails> purchaseDetailsList,
        ) {
          details.addAll(purchaseDetailsList);
          for (final purchaseDetails in purchaseDetailsList) {
            if (purchaseDetails.status == PurchaseStatus.canceled) {
              completer.complete(purchaseDetails.status);
              subscription.cancel();
            }
          }
        });
        final purchaseParam = AppStorePurchaseParam(
          productDetails: AppStoreProductDetails.fromSKProduct(
            dummyProductWrapper,
          ),
          applicationUserName: 'appName',
        );
        await iapStoreKitPlatform.buyNonConsumable(
          purchaseParam: purchaseParam,
        );

        final PurchaseStatus purchaseStatus = await completer.future;
        expect(purchaseStatus, PurchaseStatus.canceled);
      },
    );

    test(
      'should get canceled purchase status when error code is SKErrorOverlayCancelled',
      () async {
        fakeStoreKitPlatform.testTransactionCancel = 15;
        final details = <PurchaseDetails>[];
        final completer = Completer<PurchaseStatus>();

        final Stream<List<PurchaseDetails>> stream =
            iapStoreKitPlatform.purchaseStream;
        late StreamSubscription<List<PurchaseDetails>> subscription;
        subscription = stream.listen((
          List<PurchaseDetails> purchaseDetailsList,
        ) {
          details.addAll(purchaseDetailsList);
          for (final purchaseDetails in purchaseDetailsList) {
            if (purchaseDetails.status == PurchaseStatus.canceled) {
              completer.complete(purchaseDetails.status);
              subscription.cancel();
            }
          }
        });
        final purchaseParam = AppStorePurchaseParam(
          productDetails: AppStoreProductDetails.fromSKProduct(
            dummyProductWrapper,
          ),
          applicationUserName: 'appName',
        );
        await iapStoreKitPlatform.buyNonConsumable(
          purchaseParam: purchaseParam,
        );

        final PurchaseStatus purchaseStatus = await completer.future;
        expect(purchaseStatus, PurchaseStatus.canceled);
      },
    );

    test(
      'buying non consumable, should be able to purchase multiple quantity of one product',
      () async {
        final details = <PurchaseDetails>[];
        final completer = Completer<List<PurchaseDetails>>();
        final Stream<List<PurchaseDetails>> stream =
            iapStoreKitPlatform.purchaseStream;
        late StreamSubscription<List<PurchaseDetails>> subscription;
        subscription = stream.listen((
          List<PurchaseDetails> purchaseDetailsList,
        ) {
          details.addAll(purchaseDetailsList);
          for (final purchaseDetails in purchaseDetailsList) {
            if (purchaseDetails.pendingCompletePurchase) {
              iapStoreKitPlatform.completePurchase(purchaseDetails);
              completer.complete(details);
              subscription.cancel();
            }
          }
        });
        final productDetails = AppStoreProductDetails.fromSKProduct(
          dummyProductWrapper,
        );
        final purchaseParam = AppStorePurchaseParam(
          productDetails: productDetails,
          quantity: 5,
          applicationUserName: 'appName',
        );
        await iapStoreKitPlatform.buyNonConsumable(
          purchaseParam: purchaseParam,
        );
        await completer.future;
        expect(
          fakeStoreKitPlatform.finishedTransactions.first.payment.quantity,
          5,
        );
      },
    );

    test(
      'buying consumable, should be able to purchase multiple quantity of one product',
      () async {
        final details = <PurchaseDetails>[];
        final completer = Completer<List<PurchaseDetails>>();
        final Stream<List<PurchaseDetails>> stream =
            iapStoreKitPlatform.purchaseStream;
        late StreamSubscription<List<PurchaseDetails>> subscription;
        subscription = stream.listen((
          List<PurchaseDetails> purchaseDetailsList,
        ) {
          details.addAll(purchaseDetailsList);
          for (final purchaseDetails in purchaseDetailsList) {
            if (purchaseDetails.pendingCompletePurchase) {
              iapStoreKitPlatform.completePurchase(purchaseDetails);
              completer.complete(details);
              subscription.cancel();
            }
          }
        });
        final productDetails = AppStoreProductDetails.fromSKProduct(
          dummyProductWrapper,
        );
        final purchaseParam = AppStorePurchaseParam(
          productDetails: productDetails,
          quantity: 5,
          applicationUserName: 'appName',
        );
        await iapStoreKitPlatform.buyConsumable(purchaseParam: purchaseParam);
        await completer.future;
        expect(
          fakeStoreKitPlatform.finishedTransactions.first.payment.quantity,
          5,
        );
      },
    );

    test(
      'buying non consumable with discount, should get purchase objects in the purchase update callback',
      () async {
        final details = <PurchaseDetails>[];
        final completer = Completer<List<PurchaseDetails>>();
        final Stream<List<PurchaseDetails>> stream =
            iapStoreKitPlatform.purchaseStream;

        late StreamSubscription<List<PurchaseDetails>> subscription;
        subscription = stream.listen((
          List<PurchaseDetails> purchaseDetailsList,
        ) {
          details.addAll(purchaseDetailsList);
          if (purchaseDetailsList.first.status == PurchaseStatus.purchased) {
            completer.complete(details);
            subscription.cancel();
          }
        });
        final purchaseParam = AppStorePurchaseParam(
          productDetails: AppStoreProductDetails.fromSKProduct(
            dummyProductWrapper,
          ),
          applicationUserName: 'userWithDiscount',
          discount: dummyPaymentDiscountWrapper,
        );
        await iapStoreKitPlatform.buyNonConsumable(
          purchaseParam: purchaseParam,
        );

        final List<PurchaseDetails> result = await completer.future;
        expect(result.length, 2);
        expect(result.first.productID, dummyProductWrapper.productIdentifier);
        expect(
          fakeStoreKitPlatform.discountReceived,
          dummyPaymentDiscountWrapper.toMap(),
        );
      },
    );
  });

  group('complete purchase', () {
    test('should complete purchase', () async {
      final details = <PurchaseDetails>[];
      final completer = Completer<List<PurchaseDetails>>();
      final Stream<List<PurchaseDetails>> stream =
          iapStoreKitPlatform.purchaseStream;
      late StreamSubscription<List<PurchaseDetails>> subscription;
      subscription = stream.listen((List<PurchaseDetails> purchaseDetailsList) {
        details.addAll(purchaseDetailsList);
        for (final purchaseDetails in purchaseDetailsList) {
          if (purchaseDetails.pendingCompletePurchase) {
            iapStoreKitPlatform.completePurchase(purchaseDetails);
            completer.complete(details);
            subscription.cancel();
          }
        }
      });
      final purchaseParam = AppStorePurchaseParam(
        productDetails: AppStoreProductDetails.fromSKProduct(
          dummyProductWrapper,
        ),
        applicationUserName: 'appName',
      );
      await iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam);
      final List<PurchaseDetails> result = await completer.future;
      expect(result.length, 2);
      expect(result.first.productID, dummyProductWrapper.productIdentifier);
      expect(fakeStoreKitPlatform.finishedTransactions.length, 1);
    });
  });

  group('purchase stream', () {
    test('Should only have active queue when purchaseStream has listeners', () {
      final Stream<List<PurchaseDetails>> stream =
          iapStoreKitPlatform.purchaseStream;
      expect(fakeStoreKitPlatform.queueIsActive, false);
      final StreamSubscription<List<PurchaseDetails>> subscription1 = stream
          .listen((List<PurchaseDetails> event) {});
      expect(fakeStoreKitPlatform.queueIsActive, true);
      final StreamSubscription<List<PurchaseDetails>> subscription2 = stream
          .listen((List<PurchaseDetails> event) {});
      expect(fakeStoreKitPlatform.queueIsActive, true);
      subscription1.cancel();
      expect(fakeStoreKitPlatform.queueIsActive, true);
      subscription2.cancel();
      expect(fakeStoreKitPlatform.queueIsActive, false);
    });
  });

  group('billing configuration', () {
    test('country_code', () async {
      const expectedCountryCode = 'CA';
      fakeStoreKitPlatform.setStoreFrontInfo(
        countryCode: expectedCountryCode,
        identifier: 'ABC',
      );
      final String countryCode = await iapStoreKitPlatform.countryCode();
      expect(countryCode, expectedCountryCode);
      // Ensure deprecated code keeps working until removed.
      expect(await iapStoreKitPlatform.countryCode(), expectedCountryCode);
    });
  });
}
